package com.chenke.subject.infra.basic.service.impl;

import com.chenke.subject.common.enums.SubjectInfoTypeEnum;
import com.chenke.subject.infra.basic.entity.EsSubjectFields;
import com.chenke.subject.infra.basic.es.EsIndexInfo;
import com.chenke.subject.infra.basic.es.EsRestClient;
import com.chenke.subject.infra.basic.es.EsSearchRequest;
import com.chenke.subject.infra.basic.es.EsSourceData;
import com.chenke.subject.common.entity.PageResult;
import com.chenke.subject.infra.basic.entity.SubjectInfoEs;
import com.chenke.subject.infra.basic.service.SubjectEsService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;

@Service
@Slf4j
public class SubjectEsServiceImpl implements SubjectEsService {

    @Override
    public boolean insert(SubjectInfoEs subjectInfoEs) {
        EsSourceData esSourceData = new EsSourceData();
        Map<String, Object> data = convert2EsSourceData(subjectInfoEs);
        //组成
        esSourceData.setDocId(subjectInfoEs.getDocId().toString());
        esSourceData.setData(data);
        return EsRestClient.insertDoc(getEsIndexInfo(), esSourceData);
    }

    private Map<String, Object> convert2EsSourceData(SubjectInfoEs subjectInfoEs) {
        Map<String, Object> data = new HashMap<>();
        data.put(EsSubjectFields.SUBJECT_ID,subjectInfoEs.getSubjectId());
        data.put(EsSubjectFields.DOC_ID, subjectInfoEs.getDocId());
        data.put(EsSubjectFields.SUBJECT_NAME, subjectInfoEs.getSubjectName());
        data.put(EsSubjectFields.SUBJECT_ANSWER, subjectInfoEs.getSubjectAnswer());
        data.put(EsSubjectFields.SUBJECT_TYPE, subjectInfoEs.getSubjectType());
        data.put(EsSubjectFields.CREATE_USER, subjectInfoEs.getCreateUser());
        data.put(EsSubjectFields.CREATE_TIME, subjectInfoEs.getCreateTime());
        return data;
    }

    @Override
    public PageResult<SubjectInfoEs> querySubjectList(SubjectInfoEs req) {

        PageResult<SubjectInfoEs> pageResult = new PageResult<>();
        EsSearchRequest esSearchRequest = createSearchListQuery(req);
        SearchResponse searchResponse = EsRestClient.searchWithTermQuery(getEsIndexInfo(), esSearchRequest);

        //处理转换数据 和高亮
        List<SubjectInfoEs> subjectInfoEsList = new LinkedList<>();
        SearchHits searchHits = searchResponse.getHits();
        //如果没查到直接返回
        if (searchHits == null || searchHits.getHits() == null) {
            pageResult.setPageNo(req.getPageNo());
            pageResult.setPageSize(req.getPageSize());
            pageResult.setRecords(subjectInfoEsList);
            pageResult.setTotal(0);
            return pageResult;
        }

        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            //将查到的数据转换为实体类
            SubjectInfoEs subjectInfoEs = convertResult(hit);
            if (Objects.nonNull(subjectInfoEs)) {
                subjectInfoEsList.add(subjectInfoEs);
            }
        }
        pageResult.setPageNo(req.getPageNo());
        pageResult.setPageSize(req.getPageSize());
        pageResult.setRecords(subjectInfoEsList);
        pageResult.setTotal(Long.valueOf(searchHits.getTotalHits().value).intValue());
        return pageResult;
    }

    private SubjectInfoEs convertResult(SearchHit hit) {
        Map<String, Object> sourceAsMap = hit.getSourceAsMap();
        if (CollectionUtils.isEmpty(sourceAsMap)) {
            return null;
        }
        SubjectInfoEs result = new SubjectInfoEs();
        result.setSubjectId(MapUtils.getLong(sourceAsMap, EsSubjectFields.SUBJECT_ID));
        result.setSubjectName(MapUtils.getString(sourceAsMap, EsSubjectFields.SUBJECT_NAME));

        result.setSubjectAnswer(MapUtils.getString(sourceAsMap, EsSubjectFields.SUBJECT_ANSWER));

        result.setDocId(MapUtils.getLong(sourceAsMap, EsSubjectFields.DOC_ID));
        result.setSubjectType(MapUtils.getInteger(sourceAsMap, EsSubjectFields.SUBJECT_TYPE));
        //评分是Elasticsearch根据查询和文档的相关性计算得出的。评分越高，表示文档与查询的匹配度越高。Elasticsearch使用了一种基于TF-IDF（词频-逆文档频率）和向量空间模型的算法来计算评分。
        result.setScore(new BigDecimal(String.valueOf(hit.getScore())).multiply(new BigDecimal("100.00")
                .setScale(2, RoundingMode.HALF_UP)));

        //处理name的高亮
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        HighlightField subjectNameField = highlightFields.get(EsSubjectFields.SUBJECT_NAME);
        if(Objects.nonNull(subjectNameField)){
            Text[] fragments = subjectNameField.getFragments();
            StringBuilder subjectNameBuilder = new StringBuilder();
            for (Text fragment : fragments) {
                subjectNameBuilder.append(fragment);
            }
            result.setSubjectName(subjectNameBuilder.toString());
        }
        //处理答案高亮
        HighlightField subjectAnswerField = highlightFields.get(EsSubjectFields.SUBJECT_ANSWER);
        if(Objects.nonNull(subjectAnswerField)){
            Text[] fragments = subjectAnswerField.getFragments();
            StringBuilder subjectAnswerBuilder = new StringBuilder();
            for (Text fragment : fragments) {
                subjectAnswerBuilder.append(fragment);
            }
            result.setSubjectAnswer(subjectAnswerBuilder.toString());
        }
        return result;
    }

    private EsSearchRequest createSearchListQuery(SubjectInfoEs req) {
        EsSearchRequest esSearchRequest = new EsSearchRequest();
        BoolQueryBuilder bq = new BoolQueryBuilder();

        //匹配题目名称
        MatchQueryBuilder subjectNameQueryBuilder =
                QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_NAME, req.getKeyWord());
        //添加条件
        bq.should(subjectNameQueryBuilder);
        //添加权重 比如在题目和内容中搜索 题目的权重更高
        subjectNameQueryBuilder.boost(2);

        //匹配题目内容
        MatchQueryBuilder subjectAnswerQueryBuilder =
                QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_ANSWER, req.getKeyWord());
        bq.should(subjectAnswerQueryBuilder);

        //匹配题目的类型为简答题
        MatchQueryBuilder subjectTypeQueryBuilder =
                QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_TYPE, SubjectInfoTypeEnum.BRIEF.getCode());
        //必须匹配
        bq.must(subjectTypeQueryBuilder);
        //最少匹配到一条记录
        bq.minimumShouldMatch(1);
        /**
         * 假设你有一个 Elasticsearch 索引，其中包含了多个文档，每个文档都有标题（title）和内容（content）两个字段。现在，你执行了一个搜索查询，查询字符串是 "Elasticsearch 搜索"。
         * requireFieldMatch(true)（默认值）:
         * 如果查询字符串 "Elasticsearch 搜索" 直接匹配了某个文档的标题或内容字段，那么该字段中匹配的部分将会被高亮显示。
         * 如果查询字符串没有直接匹配到任何字段的内容（比如，某个文档的标题是 "Elasticsearch 入门"，内容是 "学习如何使用 Elasticsearch 进行搜索"），那么该文档将不会被高亮显示，因为 requireFieldMatch 默认为 true，要求查询字符串必须直接匹配字段内容。
         * requireFieldMatch(false):
         * 在这种情况下，无论查询字符串 "Elasticsearch 搜索" 是否直接匹配了某个字段的内容，Elasticsearch 都会尝试对所有字段进行高亮处理。
         * 假设有一个文档的标题是 "Elasticsearch 入门"，内容是 "学习如何使用 Elasticsearch 进行搜索"。虽然查询字符串没有直接匹配标题，但因为 requireFieldMatch 被设置为 false，Elasticsearch 仍然会尝试对内容字段进行高亮处理，可能会高亮显示 "Elasticsearch"（如果内容字段中包含了这个词）。
         * 需要注意的是，由于 requireFieldMatch(false) 允许对所有字段进行高亮处理，这可能会导致一些不期望的高亮结果，比如高亮显示了与查询字符串相关但并不完全匹配的词汇。因此，在使用时需要谨慎考虑是否适合你的应用场景。
         * **/
        //需要匹配的字段 .field("*")
        //.requireFieldMatch(false)
        HighlightBuilder highlightBuilder = new HighlightBuilder().field("*").requireFieldMatch(false);
        highlightBuilder.preTags("<span style = \"color:red\">");
        highlightBuilder.postTags("</span>");

        esSearchRequest.setBq(bq);
        esSearchRequest.setHighlightBuilder(highlightBuilder);
        esSearchRequest.setFields(EsSubjectFields.FIELD_QUERY);
        esSearchRequest.setFrom((req.getPageNo() - 1) * req.getPageSize());
        esSearchRequest.setSize(req.getPageSize());
        esSearchRequest.setNeedScroll(false);
        esSearchRequest.setSortOrder(SortOrder.DESC);
        return esSearchRequest;
    }


    private EsIndexInfo getEsIndexInfo() {
        EsIndexInfo esIndexInfo = new EsIndexInfo();
        esIndexInfo.setClusterName("es1");
        esIndexInfo.setIndexName("subject_index");
        return esIndexInfo;
    }
}
